/*
 * Copyright (c) 2013-2018 上海汇数数据. All rights reserved.
 * @(#) MLChinaID.java 2018-08-05 23:28
 */

package cn.jh.common.core.id;

import cn.jh.common.core.domain.Gender;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.inferred.freebuilder.FreeBuilder;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;

import java.time.Year;
import java.util.Map;
import java.util.regex.Pattern;


/**
 * 中国大陆居民身份证号信息。
 *
 * @author Fuchun
 * @since 1.0
 */
@FreeBuilder
abstract class MLChinaID extends ChinaID {

    private static final int MLC_ID_LENGTH = 18;
    private static final Pattern MLC_ID_PATTERN = Pattern.compile("^[1-9][0-9]{16}[0-9X]$");

    private static final long serialVersionUID = 2469511481510619570L;

    /**
     * 省、直辖市代码表
     */
    private static final ImmutableList<String> STATE_CODES = ImmutableList.of(
            "11", "12", "13", "14", "15", "21", "22", "23", "31", "32",
            "33", "34", "35", "36", "37", "41", "42", "43", "44", "45",
            "46", "50", "51", "52", "53", "54", "61", "62", "63", "64",
            "65", "71", "81", "82", "91"
    );

    /**
     * 计算第18位数值符号的第一步乘积因子。
     * 前17位数字字符，按照顺序经过转换后得到乘积因子：{@code (1 << (17 - i)) % 11 {i|0<=i<=16}}
     * 身份证前17位的数字，每一位与对应的因子相乘，并将17个乘积相加。
     */
    private static final int[] PRODUCT_FACTORS = {
            7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2
    };

    /** 二代身份证中的生日 {@code yyyyMMdd} */
    private static final DateTimeFormatter FULL_BIRTH_FORMATTER = ISODateTimeFormat.basicDate();

    /**
     * 城市代码、名称映射 {@code Map}。
     */
    private static final Map<String, String> STATE_CODE_MAP;

    static {
        ImmutableMap.Builder<String, String> stateBuilder = ImmutableMap.builder();
        stateBuilder.put("11", "北京");
        stateBuilder.put("12", "天津");
        stateBuilder.put("13", "河北");
        stateBuilder.put("14", "山西");
        stateBuilder.put("15", "内蒙古");
        stateBuilder.put("21", "辽宁");
        stateBuilder.put("22", "吉林");
        stateBuilder.put("23", "黑龙江");
        stateBuilder.put("31", "上海");
        stateBuilder.put("32", "江苏");
        stateBuilder.put("33", "浙江");
        stateBuilder.put("34", "安徽");
        stateBuilder.put("35", "福建");
        stateBuilder.put("36", "江西");
        stateBuilder.put("37", "山东");
        stateBuilder.put("41", "河南");
        stateBuilder.put("42", "湖北");
        stateBuilder.put("43", "湖南");
        stateBuilder.put("44", "广东");
        stateBuilder.put("45", "广西");
        stateBuilder.put("46", "海南");
        stateBuilder.put("50", "重庆");
        stateBuilder.put("51", "四川");
        stateBuilder.put("52", "贵州");
        stateBuilder.put("53", "云南");
        stateBuilder.put("54", "西藏");
        stateBuilder.put("61", "陕西");
        stateBuilder.put("62", "甘肃");
        stateBuilder.put("63", "青海");
        stateBuilder.put("64", "宁夏");
        stateBuilder.put("65", "新疆");
        stateBuilder.put("71", "台湾");
        stateBuilder.put("81", "香港");
        stateBuilder.put("82", "澳门");
        stateBuilder.put("91", "国外");
        STATE_CODE_MAP = stateBuilder.build();
    }

    @Nullable
    private static LocalDate toLocalDate(@NotNull String birth) {
        try {
            if (birth.length() == 8) {
                return LocalDate.parse(birth, FULL_BIRTH_FORMATTER);
            } else {
                return null;
            }
        } catch (IllegalArgumentException ex) {
            return null;
        }
    }

    /**
     * 计算加权和。
     * 身份证前17位数字分别乘以{@link #PRODUCT_FACTORS} 对应的因子，并求和。
     *
     * @param input 身份证前17位数字组。
     * @return 返回身份证前17位的加权和。
     */
    @Contract(pure = true)
    private static int sumWeightFactor(char[] input) {
        // assert input.length == 17; 这里无需进行数据校验
        int x, sum = 0;
        for (int i = 0; i < PRODUCT_FACTORS.length; i++) {
            x = input[i] - '0';
            sum += x * PRODUCT_FACTORS[i];
        }
        return sum;
    }

    @Nullable
    private static String getStateCode(@NotNull String id) {
        String sc = id.substring(0, 2);
        if (!STATE_CODES.contains(sc)) {
            return null;
        }
        return sc;
    }

    @Contract("null -> null")
    private static String getStateName(String sc) {
        if (sc == null) return null;
        return STATE_CODE_MAP.get(sc);
    }

    private static LocalDate getBirthday(@NotNull String id) {
        String birth = id.substring(6, 14);
        return toLocalDate(birth);
    }

    @Contract(pure = true)
    private static Gender getGender(@NotNull String id) {
        int c17 = id.charAt(16) - '0';
        return c17 % 2 == 0 ? Gender.FEMALE : Gender.MALE;
    }

    private static boolean validLastCode(@NotNull String id) {
        int sum = sumWeightFactor(id.substring(0, 17).toCharArray());
        int n = (12 - (sum % 11)) % 11;
        if (n < 10) {
            return id.charAt(17) - '0' == n;
        } else {
            return id.charAt(17) == 'X';
        }
    }

    @Contract("null -> false")
    private static boolean baseValidId(String id) {
        return id != null && id.length() == MLC_ID_LENGTH &&
                MLC_ID_PATTERN.matcher(id).matches();
    }

    public static boolean validId(String id) {
        return baseValidId(id) &&
                getStateCode(id) != null && getBirthday(id) != null && validLastCode(id);
    }

    @Contract("null -> fail")
    public static MLChinaID parse(@NotNull String id) {
        boolean bv = baseValidId(id);
        String sc = getStateCode(id);
        String scName = getStateName(sc);
        String ac = id.substring(2, 6);
        LocalDate birthDate = getBirthday(id);
        Gender gender = getGender(id);
        Builder builder = new Builder(id)
                .setStateCode(sc)
                .setStateName(scName)
                .setAreaCode(ac)
                .setBirthday(birthDate)
                .setGender(gender);
        if (!bv || sc == null || birthDate == null || !validLastCode(id)) {
            return builder.setAge(-1).setValid(false).build();
        }
        int age = Year.now().minusYears(birthDate.getYear()).getValue();
        return builder.setAge(age).setValid(true).build();
    }

    static class Builder extends MLChinaID_Builder {
        Builder(String id) {
            setNumber(id);
        }
    }

    @Nullable
    public abstract String getStateName();

    @Contract(pure = true)
    @Override
    public final int getValidLength() {
        return MLC_ID_LENGTH;
    }
}
